﻿using System;
using System.Diagnostics;
using System.Text;
using System.Threading.Tasks;
using DevelopersTestPad.AppFrame;
using DevelopersTestPad.Remoting;
using Ooaavee.Scripting;

namespace DevelopersTestPad.Worksheets.DotNet.Interaction
{
    internal sealed class ScriptWrapper : IDisposable
    {
        private Script _script;
        private readonly ConsoleWriter _consoleWriter = new ConsoleWriter();
        private readonly WorksheetType _worksheetType;
        private readonly string _codeLines;

        /// <summary>
        /// Occurs when a build has started.
        /// </summary>
        public event EventHandler<BuildStartedEventArgs> BuildStarted;

        /// <summary>
        /// Occurs when a build has failed. 
        /// </summary>
        public event EventHandler<BuildFailedEventArgs> BuildFailed;

        /// <summary>
        /// Occurs when a build succeed.
        /// </summary>
        public event EventHandler<BuildSucceedEventArgs> BuildSucceed;

        /// <summary>
        /// Occurs when a run has been started. 
        /// </summary>
        public event EventHandler<RunStartedEventArgs> RunStarted;

        /// <summary>
        /// Occurs when a run has been failed. 
        /// </summary>
        public event EventHandler<RunFailedEventArgs> RunFailed;

        /// <summary>
        /// Occurs when a run has been completed.
        /// </summary>
        public event EventHandler<RunCompletedEventArgs> RunCompleted;

        /// <summary>
        /// Occurs when received a plain text.
        /// </summary>
        public event EventHandler<ConsoleWriterEventArgs> ReceivePlainText;

        /// <summary>
        /// Occurs when received an interaction message.
        /// </summary>
        public event EventHandler<InteractionMessageEventArgs> ReceiveInteractionMessage;

        /// <summary>
        /// Occurs when the script execution has ended.
        /// </summary>
        public event EventHandler<ExecutionEndEventArgs> ExecutionEnd; 

        /// <summary>
        /// Creates a new script wrapper.
        /// </summary>
        /// <param name="worksheetType">The worksheet type.</param>
        /// <param name="codeLines">The code lines.</param>
        public ScriptWrapper(WorksheetType worksheetType, string codeLines)
        {
            _worksheetType = worksheetType;
            _codeLines = codeLines;
            _consoleWriter.ReceivePlainText += (sender, e) => OnReceivePlainText(e);
        }

        /// <summary>
        /// Executes the script asynchronously.
        /// </summary>
        public async Task ExecuteAsync()
        {
            Task.Factory.StartNew(() => Execute());
        }

        /// <summary> 
        /// Clean up any resources being used.
        /// </summary>
        public void Dispose()
        {
            if (_script != null)
            {
                _script.Dispose();
            }
        }

        /// <summary>
        /// Executes the script.
        /// </summary>
        private void Execute()
        {
            using (WorkerScope.CreateNew())
            {
                try
                {
                    ScriptingContext context = AppContext.Current.CreateScriptingContext(Language);

                    // Compile.
                    OnBuildStarted(new BuildStartedEventArgs {Message = ToBuildStartedMessage(Language)});
                    Stopwatch cw = new Stopwatch();
                    try
                    {
                        cw.Start();
                        _script = Script.Compile(_codeLines, context);
                        cw.Stop();
                    }
                    catch (ScriptCompileException ex)
                    {
                        OnBuildFailed(new BuildFailedEventArgs {Exception = ex, Message = ToBuildFailedMessage(ex)});
                        return;
                    }
                    OnBuildSucceed(new BuildSucceedEventArgs {Message = ToBuildSucceedMessage(cw.Elapsed)});

                    // Run.
                    OnRunStarted(new RunStartedEventArgs {Message = ToRunStartedMessage()});
                    Stopwatch rw = new Stopwatch();
                    object returnValue;
                    try
                    {
                        rw.Start();
                        returnValue = Script.Run(_script, GetParameters());
                        rw.Stop();
                    }
                    catch (ScriptRunException ex)
                    {
                        rw.Stop();
                        OnRunFailed(new RunFailedEventArgs { Exception = ex, Message = ToRunFailedMessage(rw.Elapsed, ex) });
                        return;
                    }

                    if (returnValue != null)
                    {
                        OnReceiveInteractionMessage(new InteractionMessageEventArgs { Data = returnValue, Type = InteractionMessageEventArgs.InteractionMessageType.Text });                        
                    }

                    OnRunCompleted(new RunCompletedEventArgs { ReturnValue = returnValue, Message = ToRunCompletedMessage(rw.Elapsed) });
                }
                finally
                {
                    OnExecutionEnd(new ExecutionEndEventArgs());
                }
            }
        }

        /// <summary>
        /// Gets the targeted programming language.
        /// </summary>
        private Language Language
        {
            get
            {
                switch (_worksheetType)
                {
                    case WorksheetType.CSharpScript:
                        return Language.CSharp;
                    case WorksheetType.VisualBasicScript:
                        return Language.VisualBasic;
                    default:
                        throw new NotImplementedException();
                }
            }
        }

        /// <summary>
        /// Returns parameters for the script.
        /// </summary>
        /// <returns>Parameters.</returns>
        private object GetParameters()
        {
            DevelopersTestPadParameters parameters = new DevelopersTestPadParameters();

            parameters.ConsoleStandardOutputStream = _consoleWriter;

            parameters.ReceiveInteractionMessage += (sender, e) => OnReceiveInteractionMessage(e);

            return parameters;
        }

        /// <summary>
        /// Raises the <see cref="BuildStarted"/> event.
        /// </summary>
        /// <param name="e">An <see cref="BuildStartedEventArgs"/> that contains the event data.</param>
        private void OnBuildStarted(BuildStartedEventArgs e)
        {
            EventHandler<BuildStartedEventArgs> handler = BuildStarted;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        /// <summary>
        /// Raises the <see cref="BuildFailed"/> event.
        /// </summary>
        /// <param name="e">An <see cref="BuildFailedEventArgs"/> that contains the event data.</param>
        private void OnBuildFailed(BuildFailedEventArgs e)
        {
            EventHandler<BuildFailedEventArgs> handler = BuildFailed;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        /// <summary>
        /// Raises the <see cref="BuildSucceed"/> event.
        /// </summary>
        /// <param name="e">An <see cref="BuildSucceedEventArgs"/> that contains the event data.</param>
        private void OnBuildSucceed(BuildSucceedEventArgs e)
        {
            EventHandler<BuildSucceedEventArgs> handler = BuildSucceed;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        /// <summary>
        /// Raises the <see cref="RunStarted"/> event.
        /// </summary>
        /// <param name="e">An <see cref="RunStartedEventArgs"/> that contains the event data.</param>
        private void OnRunStarted(RunStartedEventArgs e)
        {
            EventHandler<RunStartedEventArgs> handler = RunStarted;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        /// <summary>
        /// Raises the <see cref="RunFailed"/> event.
        /// </summary>
        /// <param name="e">An <see cref="RunFailedEventArgs"/> that contains the event data.</param>
        private void OnRunFailed(RunFailedEventArgs e)
        {
            EventHandler<RunFailedEventArgs> handler = RunFailed;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        /// <summary>
        /// Raises the <see cref="RunCompleted"/> event.
        /// </summary>
        /// <param name="e">An <see cref="RunCompletedEventArgs"/> that contains the event data.</param>
        private void OnRunCompleted(RunCompletedEventArgs e)
        {
            EventHandler<RunCompletedEventArgs> handler = RunCompleted;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        /// <summary>
        /// Raises the <see cref="ReceivePlainText"/> event.
        /// </summary>
        /// <param name="e">An <see cref="ConsoleWriterEventArgs"/> that contains the event data.</param>
        private void OnReceivePlainText(ConsoleWriterEventArgs e)
        {
            EventHandler<ConsoleWriterEventArgs> handler = ReceivePlainText;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        /// <summary>
        /// Raises the <see cref="ReceiveInteractionMessage"/> event.
        /// </summary>
        /// <param name="e">An <see cref="InteractionMessageEventArgs"/> that contains the event data.</param>
        private void OnReceiveInteractionMessage(InteractionMessageEventArgs e)
        {
            EventHandler<InteractionMessageEventArgs> handler = ReceiveInteractionMessage;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        /// <summary>
        /// Raises the <see cref="ExecutionEnd"/> event.
        /// </summary>
        /// <param name="e">An <see cref="ExecutionEndEventArgs"/> that contains the event data.</param>
        private void OnExecutionEnd(ExecutionEndEventArgs e)
        {
            EventHandler<ExecutionEndEventArgs> handler = ExecutionEnd;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        /// <summary>
        /// Creates a build started message.
        /// </summary>
        /// <param name="language">The programming language.</param>
        /// <returns>The build started message.</returns>
        private static string ToBuildStartedMessage(Language language)
        {
            switch (language)
            {
                case Language.CSharp:
                    return "Compile started; C# script.";
                case Language.VisualBasic:
                    return "Compile started; VB script.";
                default:
                    throw new ArgumentOutOfRangeException("language");
            }
        }

        /// <summary>
        /// Creates a build failed message.
        /// </summary>
        /// <param name="exception">The script compile exception.</param>
        /// <returns>The build failed message.</returns>
        private static string ToBuildFailedMessage(ScriptCompileException exception)
        {
            return "Compile failed; see the Compile Errors tab for more details.";
        }

        /// <summary>
        /// Creates a build succeed message to show to the user.
        /// </summary>
        /// <param name="compileTime">The compile time.</param>
        /// <returns>The build succeed message.</returns>
        private static string ToBuildSucceedMessage(TimeSpan compileTime)
        {
            return "Compile succeed; " + string.Format("the compile time was {0}.", ToElapsedTimeString(compileTime));
        }

        /// <summary>
        /// Creates a run started message.
        /// </summary>
        /// <returns>The run started message.</returns>
        private static string ToRunStartedMessage()
        {
            return "Execution started.";
        }

        /// <summary>
        /// Creates a run failed message.
        /// </summary>
        /// <param name="runTime">The script run time.</param>
        /// <param name="exception">The script run exception.</param>
        /// <returns>The run failed message.</returns>
        private static string ToRunFailedMessage(TimeSpan runTime, ScriptRunException exception)
        {
            return ToMultiline(string.Format("{0}: {1}", exception.InnerException.GetType().FullName, exception.InnerException.Message),
                               "Execution failed; see the Runtime Exceptions tab for more details.");
        }

        /// <summary>
        /// Creates a run completed message.
        /// </summary>
        /// <param name="runTime">The script run time.</param>
        /// <returns>The run completed message.</returns>
        private static string ToRunCompletedMessage(TimeSpan runTime)
        {
            return "Execution completed; " + string.Format("the execution time was {0}.", ToElapsedTimeString(runTime));
        }

        /// <summary>
        /// Gets the elapsed time string.
        /// </summary>
        /// <param name="span">TimeSpan.</param>
        /// <returns>The elapsed time string.</returns>
        private static string ToElapsedTimeString(TimeSpan span)
        {
            int seconds = span.Seconds;
            seconds = seconds + span.Minutes * 60;
            seconds = seconds + span.Hours * 60 * 60;
            seconds = seconds + span.Days * 24 * 60 * 60;

            int milliseconds = span.Milliseconds;

            string message;

            if (seconds == 0)
            {
                message = string.Format("{0} milliseconds", milliseconds);
            }
            else if (seconds == 1)
            {
                message = string.Format("{0} second {1} milliseconds", seconds, milliseconds);
            }
            else
            {
                message = string.Format("{0} seconds {1} milliseconds", seconds, milliseconds);
            }

            return message;
        }

        private static string ToMultiline(params string[] lines)
        {
            StringBuilder s = new StringBuilder();

            for (int i = 0; i < lines.Length; i++)
            {
                if (i > 0)
                {
                    s.Append(Environment.NewLine);
                }
                s.Append(lines[i]);
            }

            return s.ToString();
        }

        /// <summary>
        /// Provides data for the <see cref="BuildStarted"/> event.
        /// </summary>
        public sealed class BuildStartedEventArgs : EventArgs
        {
            public string Message { set; get; }
        }

        /// <summary>
        /// Provides data for the <see cref="BuildFailed"/> event.
        /// </summary>
        public sealed class BuildFailedEventArgs : EventArgs
        {
            public string Message { set; get; }
            public ScriptCompileException Exception { set; get; }
        }

        /// <summary>
        /// Provides data for the <see cref="BuildSucceed"/> event.
        /// </summary>
        public sealed class BuildSucceedEventArgs : EventArgs
        {
            public string Message { set; get; }            
        }

        /// <summary>
        /// Provides data for the <see cref="RunStarted"/> event.
        /// </summary>
        public sealed class RunStartedEventArgs : EventArgs
        {
            public string Message { set; get; }                        
        }

        /// <summary>
        /// Provides data for the <see cref="RunFailed"/> event.
        /// </summary>
        public sealed class RunFailedEventArgs : EventArgs
        {
            public string Message { set; get; }
            public ScriptRunException Exception { set; get; }            
        }

        /// <summary>
        /// Provides data for the <see cref="RunCompleted"/> event.
        /// </summary>
        public sealed class RunCompletedEventArgs : EventArgs
        {
            public string Message { set; get; }
            public object ReturnValue { set; get; }
        }

        /// <summary>
        /// Provides data for the <see cref="ExecutionEnd"/> event.
        /// </summary>
        public sealed class ExecutionEndEventArgs : EventArgs
        {            
        }
    }
}
